<?php

Raise::load('core.RaiseDataType');
Raise::load('core.helper.sorting.IRaiseComparable');
Raise::load('RaiseTimeSpan');
Raise::load('RaiseDateTimeComparator');
Raise::load('RaiseDate');
Raise::load('RaiseTime');
Raise::load('RaiseDateTimeFormatter');

/**
 * RaiseDateTime class
 * representing date and time
 *
 * @author Sam-Mauris Yong / hellclanner at live dot com
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package phpraise.core.datetime
 * @since 1.2
 */
class RaiseDateTime extends RaiseDataType implements IRaiseComparable {
    
    /**
     * Saturday
     */
    const SATURDAY = 0;
    
    /**
     * Sunday
     */
    const SUNDAY = 1;
    
    /**
     * Monday
     */
    const MONDAY = 2;
    
    /**
     * Tuesday
     */
    const TUESDAY = 3;
    
    /**
     * Wednesday
     */
    const WEDNESDAY = 4;
    
    /**
     * Thursday
     */
    const THURSDAY = 5;
    
    /**
     * Friday
     */
    const FRIDAY = 6;
    
    /**
     * January
     */
    const JANUARY = 1;
    
    /**
     * February
     */
    const FEBRUARY = 2;
    
    /**
     * March
     */
    const MARCH = 3;
    
    /**
     * April
     */
    const APRIL = 4;
    
    /**
     * May
     */
    const MAY = 5;
    
    /**
     * June
     */
    const JUNE = 6;
    
    /**
     * July
     */
    const JULY = 7;
    
    /**
     * August
     */
    const AUGUST = 8;
    
    /**
     * September
     */
    const SEPTEMBER = 9;
    
    /**
     * October
     */
    const OCTOBER = 10;
    
    /**
     * November
     */
    const NOVEMBER = 11;
    
    /**
     * December
     */
    const DECEMBER = 12;

    /**
     * The date component
     * @var RaiseDate 
     */
    private $date;
    
    /**
     * The time component
     * @var RaiseTime
     */
    private $time;
    
    /**
     * The timezone component of the RaiseDateTime
     * @var double
     */
    private $timezone = 0;
    
    public function __construct($year, $month, $day, $hour = 0, $minute = 0, $second = 0, $millisecond = 0){
        $this->date = new RaiseDate($year, $month, $day);
        $this->time = new RaiseTime($hour, $minute, $second, $millisecond);
    }

    /**
     * Get or set the timezone component of the RaiseDateTime
     * @param double $a An integer that represents number of hours in terms of time zone offset (1.5 for 1 hour 30 minutes)
     * @return double
     */
    public function timezone($a = null){
        if(func_num_args() == 1 && $this->timezone != $a){
            $dt = self::convertTimezone($this, $a);
            $this->date = $dt->date;
            $this->time = $dt->time;
            $this->timezone = $dt->timezone;
            unset($dt);
        }
        return $this->timezone;
    }
    
    /**
     * Get the date component
     * @return RaiseDate
     */
    public function date(){
        return $this->date;
    }
    
    /**
     * Get the time component
     * @return RaiseTime
     */
    public function time(){
        return $this->time;
    }

    /**
     * Get the RaiseDateTime of this exact instant
     * @return RaiseDateTime
     * @static
     */
    public static function now() {
        static $now = null;
        if(!$now){
            $now = self::fromTimestamp(false);
        }
        return $now;
    }
    
    /**
     * Get the day of the week based on Zeller's Congruence
     * @return integer Returns one of the Day of week constants
     */
    public function dayOfWeek(){
        $month = $this->date->month();
        $year = $this->date->year();
        if($month < 3){
            $month += 12;
            $year--;
        }
        $century = (int)($year / 100);
        $year = $year % 100;
        $t = $this->date->day() + (int)(26 * ($month + 1) / 10) + $year + (int)($year / 4) + (int)($century / 4) - 2 * $century;
        $dow = $t % 7;
        if($dow < 0){
            $dow += 7;
        }
        return $dow;
    }

    /**
     * Convert a UNIX Epoch Timestamp to RaiseDateTime
     * Note that when converting from a seconds-based timestamp to RaiseDateTime, timezone and millisecond information cannot be captured and will be both set to 0.
     * @param integer $ts UNIX Epoch Timestamp. If not set, the conversion will be done from the current time.
     * @return RaiseDateTime
     * @TODO need a workaround to parse format and prevent reliance on gmdate() and timestamps
     * @static
     */
    public static function fromTimestamp($ts) {
        if (!$ts) {
            $ts = time();
        }
        $dt = new RaiseDateTime(
                (int) gmdate('o', $ts),
                (int) gmdate('n', $ts),
                (int) gmdate('j', $ts),
                (int) gmdate('G', $ts),
                (int) gmdate('i', $ts),
                (int) gmdate('s', $ts)
            );
        
        return $dt;
    }

    /**
     * Change the timezone of a RaiseDateTime
     * @param RaiseDateTime $dateTime The date time object
     * @param double $target The destination timezone to set the date time to. In the form of hours from -12 to 12
     * return RaiseDateTime
     * @static
     */
    public static function convertTimezone($dateTime, $target){
        if($dateTime instanceof RaiseDateTime){
            if($target < -12 || $target > 12){
                throw new RaiseInvalidArgumentException('$target Timezone must be between -12 and 12 inclusive when converting between timezones');
            }else{
                if($dateTime->timezone == $target){
                    $clone = $dateTime->raiseClone();
                    $clone->date = $clone->date->raiseClone();
                    $clone->time = $clone->time->raiseClone();
                    return $clone;
                }
                $diff = $dateTime->timezone - $target;
                $ts = new RaiseTimeSpan(abs($diff) * 3600);
                if($diff > 0){
                    $dt = $dateTime->subtract($ts);
                }else{
                    $dt = $dateTime->add($ts);
                }
                $dt->timezone = $target + 0;
                return $dt;
            }
        }else{
            throw new RaiseInvalidArgumentException('$dateTime must be a RaiseDateTime instance when converting between timezones.');
        }
    }

    /**
     * Converts a string into RaiseDateTime
     * @param string $s The string to convert
     * @return RaiseDateTime
     * @static
     */
    public static function fromString($s) {
        return self::fromTimestamp(strtotime($s));
    }

    /**
     * Converts this RaiseDateTime object to UNIX Epoch Timestamp at GMT
     * @param boolean $convert (optional) Set whether the RaiseDateTime is converted to GMT before returning the number of seconds from the UNIX Epoch. Defaults to true.
     * @return integer
     */
    public function toTimestamp($convert = true) {
        $neutral = $this;
        if($convert){
            $neutral = self::convertTimezone($neutral, 0);
        }
        $ts = gmmktime($neutral->time->hour(), $neutral->time->minute(), $neutral->time->second(), $neutral->date->month(), $neutral->date->day(), $neutral->date->year());
        return $ts;
    }

    /**
     * Creates an ISO8601 RaiseDateTime string
     * @link http://www.iso.org/iso/date_and_time_format
     * @return string
     */
    public function toISO8601() {
        return gmdate('Y-m-d\TH:i:s', $this->toTimestamp()) .
                ($this->timezone > 0 ?
                '+' . sprintf('%02d', floor(abs($this->timezone))) . ':' . sprintf('%02d', ceil(abs($this->timezone)-floor(abs($this->timezone))) * 60) :
                '-' . sprintf('%02d', floor(abs($this->timezone))) . ':' . sprintf('%02d', ceil(abs($this->timezone)-floor(abs($this->timezone))) * 60)  );
    }

    /**
     * Creates an RFC822 (updated by RFC 1123) RaiseDateTime formatted string
     * @link http://www.freesoft.org/CIE/RFC/1945/14.htm
     * @return string
     */
    public function toRFC822(){
       return gmdate(RaiseDateTimeFormatter::RFC822, $this->toTimestamp());
    }

    /**
     * Check if a year is leap year
     * @param integer|RaiseDateTime $year The year or RaiseDateTime to check
     * @return boolean TRUE if the year is a leap year, FALSE otherwise
     * @static
     */
    public static function isLeapYear($year){
        if($year instanceof RaiseDateTime){
            $year = $year->date->year();
        }
        return (($year % 4 == 0) && ($year % 100 != 0) || ($year % 400 == 0));
    }

    /**
     * Get the number of days in a month, considering whether the year is a leap year or not
     * @param integer|RaiseDateTime $month The month of the year (1-12) or the RaiseDateTime to check
     * @param integer $year (optional) The year. Ignored when first parameter is RaiseDateTime
     * @return integer The number of days in that month of that year
     * @static
     */
    public static function daysInMonth($month, $year = null){
        if($month instanceof RaiseDateTime){
            $year = $month->date->year();
            $month = $month->date->month();
        }elseif(func_num_args() == 1){
            $year = RaiseDateTime::now()->date->year();
        }
        if($month < 1 || $month > 12){
            throw new RaiseInvalidArgumentException('RaiseDateTime::daysInMonth() expects first parameter $month to be from 1 to 12 inclusive.');
        }
        $mapping = array(
            31, (self::isLeapYear($year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        );
        if(array_key_exists($month - 1, $mapping)){
            return $mapping[$month - 1];
        }        
    }

    /**
     * Add a period of time RaiseTimeSpan to the current date time
     * @param RaiseTimeSpan $dt The RaiseTimeSpan to add
     * @return RaiseDateTime The result RaiseDateTime that resulted from the add operation.
     */
    public function add($dt){
        if($dt instanceof RaiseTimeSpan){
            $temp = $this->raiseClone();
            $temp->time = $temp->time->raiseClone();
            $temp->date = $temp->date->raiseClone();
            $addTime = new RaiseTime($dt->hour(), $dt->minute(), $dt->second(), $dt->millisecond());
            if($temp->time->totalSeconds() + $addTime->totalSeconds() > 86400){
                $temp->date->day($temp->date->day() + 1);
            }
            $temp->time = $temp->time->add($addTime);
            $temp->date->day($temp->date->day() + $dt->day());
            return $temp;
        }else{
            throw new RaiseInvalidArgumentException('RaiseDateTime::add() expecting 1st parameter to be RaiseTimeSpan.');
        }
    }

    /**
     * Subtract another RaiseDateTime or RaiseTimeSpan from the current one
     * @param RaiseDateTime|RaiseTimeSpan $dt The RasieDateTime or RaiseTimeSpan to subtract
     * @return RaiseTimeSpan|RaiseDateTime The result of the subtract operation. If $dt is a RaiseDateTime, RaiseTimeSpan will be returned. If $dt is a RaiseTimeSpan, RaiseDateTime will be returned instead.
     */
    public function subtract($dt){
        $temp = $this->raiseClone();
        $temp->time = $temp->time->raiseClone();
        $temp->date = $temp->date->raiseClone();
        if($dt instanceof RaiseDateTime){
            if($this->compareTo($dt) == 1){
                throw new RaiseInvalidArgumentException('Cannot subtract a date from a date earlier.');
            }
            $tempTime = $dt->time;
            $tempDate = $dt->date;
            if($temp->time->totalSeconds() - $dt->time->totalSeconds() < 0){
                $temp->date->day($temp->date->day() - 1);
            }
        }elseif($dt instanceof RaiseTimeSpan){
            $tempTime = new RaiseTime($dt->hour(), $dt->minute(), $dt->second());
            if($temp->time->totalSeconds() - $tempTime->totalSeconds() < 0){
                $temp->date->day($temp->date->day() - 1);
            }
            $tempDate = $dt;
        }
        $temp->time = $temp->time->subtract($tempTime);
        $temp->date = $temp->date->subtract($tempDate);
        if($dt instanceof RaiseDateTime){
            $timespan = new RaiseTimeSpan();
            $timespan->hour($temp->time->hour());
            $timespan->minute($temp->time->minute());
            $timespan->second($temp->time->second());
            $timespan->millisecond($temp->time->millisecond());
            $timespan->day($temp->date->totalDays());
            return $timespan;
        }
        return $temp;
    }

    /**
     * Formats the RaiseDateTime object and return as string
     * @param string $format
     * @return string
     * @TODO need a workaround to parse format and prevent reliance on gmdate() and timestamps
     */
    public function toFormat($format) {
        $fmtter = new RaiseDateTimeFormatter($format);
        return $fmtter->parse($this);
    }
    
    /**
     * Calculate the age based on birthday and current date
     * @param RaiseDateTime $birthday
     * return integer The age calculated
     * @static
     */
    public static function calculateAge($birthday){
        $now = RaiseDateTime::now();
        $bday = $birthday->raiseClone();
        $years = $now->date->year() - $bday->date->year();
        $bday->date->year($now->date->year());
        
        if($now->compareTo($bday) == 1){
            /**
             * We check whether birthday has passed for this year. If not subtract one.
             */
            $years--;
        }
        
        return $years;
    }

    /**
     * Returns a UNIX Timestamp with microseconds
     * @return double
     * @static
     */
    public static function microtime(){
        $usec = 0;
        $sec = 0;
        list($usec, $sec) = explode(' ', microtime());
        return ((double)$usec + (double)$sec);
    }

    /**
     * Compare to another RaiseDateTime
     * @param RaiseDateTime $date
     * @return integer The comparison result: 1 if $date is more than $this, 0 if equal, and -1 if $this is more than $date.
     */
    public function compareTo($date) {
        $comparator = new RaiseDateTimeComparator();
        return $comparator->compare($this, $date);
    }


}